API/События
Введение Событийная модель построения игры очень удобна. Если быть точнее, то такая модель не обязательна, но она имеется в игре, и ею можно пользоваться для удобства создания своих модов и понимания кода игры. Общий принцип такой. Каждый префаб может принимать события. На каждое событие может приходиться несколько слушателей. Эти слушатели заблаговременно отметились (зарегистрировались) в специальном списке (таблице), поэтому сразу после события фокус управления передается по очереди всем слушателям. Итак, давайте рассмотрим подробнее, как и что работает. Что такое событие вообще? Событие - это некая логическая точка передачи управления, которая имеет почти уникальное название и происходит в конкретный момент времени. К событию могут быть прикреплены другие данные, относящиеся к нему (различные связи, обстоятельства). Главное свойство события - его смысловая уникальность. Если какой-то код следит за событием, то он узнает о нем не раньше и не позже самого события, а также ровно столько раз, сколько произойдет само событие по смыслу. Здесь важно понимать, что нет никаких строгих ограничений. Событийная модель помогает вам лучше поднять игру и свой код. Разрабатывая компонент, вы можете не думать о том, как устроены другие компоненты, а также как и кем будет использоваться ваш компонент. Название события - это 1-2 слова, которые кратко и ёмко формулируют, что произошло. Это важно. Если путаете смысл, то это может стать источником ошибок. Простой пример Самый простой пример регистрации обработчика события: inst:ListenForEvent("attacked", OnAttacked) В этом примере префаб inst регистрирует в качестве обработчика функцию OnAttacked в ответ на событие "attacked". То есть это реакция на атаку. В этом примере слушатель и источник события совпадают - это сам префаб. Можно сказать, что префаб слушает сам себя. Ниже мы разберем более сложные случаи. Обработчик может быть, например, таким: local function OnAttacked(inst, data) inst.components.combat:SetTarget(data.attacker) end Как видите, обработчику передаются два параметра - источник события и дополнительные данные (которых может и не быть). Вместе с событием "атакован" логично передать информацию о том, кто, кого и даже чем атаковал. Общая схема none Как было сказано выше, обычно источник и слушатель события совпадают. Но в общем виде это не так. На схеме источник и слушатель - не одно и то же. *source - источник события. Это префаб, в который приходит событие. *inst - слушатель события. Префаб, которому важно знать, что событие произошло. *event_listeners - таблица всех слушателей события (находится у источника). *event_listening - таблица всех источников события (находится у слушателя). *PushEvent - функция, с помощью которой можно сгенерировать событие. *ListenForEvent - функция, позволяющая зарегистрироваться на отслеживание события. *RemoveEventCallback - функция, с помощью которой можно отписаться от события. Справедливости ради стоит заметить, что event_listeners и event_listening есть (должны быть) в каждом префабе. Потому что каждый префаб может быть и часто является одновременно, как источником своих собственных событий, так и слушателем чужих событий. PushEvent - кто и как генерирует событие Событие может генерировать кто угодно путём вызова функции PushEvent. Это можете быть вы, это может быть другой разработчик модов, а также это может быть сама игра. Причем, сам код, вызывающий событие, может быть даже скрыт в Си-функциях (другими словами, у вас нет к нему доступа). Си-код знает указатель на каждый существующий экземпляр префаба и может свободно "дергать" любые его методы. Однако реализация самой функции PushEvent находится в луа коде, поэтому мы можем подсмотреть, как она устроена: function EntityScript:PushEvent(event, data) if self.event_listeners then local listeners = self.event_listenersevent if listeners then for entity, fns in pairs(listeners) do for i,fn in ipairs(fns) do fn(self, data) end end end end if self.sg then if self.sg:IsListeningForEvent(event) then if SGManager:OnPushEvent(self.sg) then self.sg:PushEvent(event, data) end end end if self.brain then self.brain:PushEvent(event, data) end end Что здесь происходит? PushEvent принимает два параметра - название события (event) и данные (data). Параметр data обычно является таблицей и содержит много подробностей. Но в теории никто не запрещает передать одно-единственное значение напрямую. Но не рекомендуется этого делать, - лучше всё же передать как таблицу с единственным элементом, потому что название ключа для этого элемента будет рассказывать о том, что это такое. Например, {attacker = inst} - здесь ясно, какой смысл несёт передаваемое значение inst. Функция PushEvent перебирает таблицу event_listeners, то есть список слушателей. Ведь именно слушателям важно знать, что событие произошло, поэтому нужно передать им управление. Причину передачи управления указывать нигде не нужно. Дело в том, что каждый слушатель заранее подсуетился и оставил прямой указатель на обработчик события. Поэтому нужно просто взывать этот обработчик. Так и происходит. fn(self, data) - это и есть вызов обработчика. Ему передается ссылка на источник и прочие данные. Ссылка на источник, как видите, в обязательном порядке приходит в любой обработчик. ListenForEvent - регистрация обработчика события Казалось бы, такая простая функция ListenForEvent, а столько подробностей в результате всплывает про неё. Просто взглянем на код. local function AddListener(t, event, inst, fn) local listeners = tevent if not listeners then listeners = {} tevent = listeners end local listener_fns = listenersinst if not listener_fns then listener_fns = {} listenersinst = listener_fns end table.insert(listener_fns, fn) end function EntityScript:ListenForEvent(event, fn, source) source = source or self if not source.event_listeners then source.event_listeners = {} end AddListener(source.event_listeners, event, self, fn) if not self.event_listening then self.event_listening = {} end AddListener(self.event_listening, event, source, fn) end На вход она получает три параметра: *event - название события (например, "attacked"). *fn - обработчик события (например, OnAttacked). *source - не обязательный источник события. Если не указан, то им является слушатель. *self - неявный параметр - указатель на сам префаб, для которого вызывается ListenForEvent (т.е. слушатель). Регистрация делится на два этапа: #Добавить обработчик в таблицу слушателей у источника. #Добавить обработчик в таблицу источников у себя, т.е. у слушателя. Второе действие кажется не очевидным. Зачем вторая таблица? Ведь при получении события она никакой роли не играет. Ответ прост. Всё верно, при получении события и вызове обработчиков она не нужна. Но она нужна при удалении. Автоматическое удаление обработчика Если какой-то экземпляр префаба удаляется - источник или слушатель, то логично также и удалить все событийные связи между ними. Если удаляется источник, то всё пронятно. Нет источника, - нет и его таблицы слушателей. Но что, если удаляется слушатель? При удалении слушателя нужно как-то найти все ссылки на все источники, где слушатель когда-либо регистрировал свои обработчики, и вычеркнуть их. Для этого и нужна вторая таблица event_listening - таблица источников. Таким образом, при удалении слушателя, менеджер событий смотрит на список источников и проверят их всех. Для каждого источника он смотрит таблицу слушателей и удаляет все ссылки на удаляемого слушателя. Аналогично происходит удаление источника. Менеджер событий проверяет у источника всех его слушателей и чистит у каждого таблицу источников. Вам не нужно следить за этим и проверять каждый раз внутри обработчика: а существует ли еще тот объект, от которого пришло событие? А существую ли "я" (слушатель события)? Если событие пришло, то источник и слушатель всё еще актуальны на 100%. Есть даже такой приём. Если вы делаете для префаба прослушивание события на самого себя, но вам нужно в обработчике следить за актуальностью другого префаба, то повесьте обработчик на него. При его удалении исчезнет и сам обработчик. Например, так делается в префабе палатки (tent.lua): sleeper:ListenForEvent("onignite", onignite, inst) Здесь inst - указатель на саму палатку, как источник события возгорания. Если палатка загорается, то спящего в ней полагается разбудить. В обработчике события сам sleeper никак не используется, ссылка на него не нужна. Однако если он выйдет из игры, то и обработчик автоматически уничтожится за ненадобностью. RemoveEventCallback - Ручное удаление обработчика Работает почти так же, как и автоматическое удаление, только удаляются не все обработчики всевозможных названий и источников, а конкретный обработчик. Просто взглянем на код: local function RemoveListener(t, event, inst, fn) if t then local listeners = tevent if listeners then local listener_fns = listenersinst if listener_fns then RemoveByValue(listener_fns, fn) if next(listener_fns) nil then listenersinst = nil end end if next(listeners) nil then tevent = nil end end end end function EntityScript:RemoveEventCallback(event, fn, source) source = source or self RemoveListener(source.event_listeners, event, self, fn) RemoveListener(self.event_listening, event, source, fn) end Чтобы удалить обработчик, нужно снова указать название события (event), сам обработчик (fn) и источник события (source). Источник можно не указывать, если он прослушивает самого себя. Здесь важно то, что сам обработчик является ключом для поиска той записи, которую надо удалить. Ведь их может быть несколько. Напоминаю, что: *Один и тот же обработчик может использоваться для разных источников. Это нормально. Например, можно повесить свой обработчик на каждого игрока, чтобы отслеживать смерть или ранения. При удалении важно указать источник, т.е. конкретного игрока, которого мы больше не желаем отслеживать. *На одном и то же источнике может висеть много разных обработчиков разных событий. Нужно знать точное название, чтобы удалять. *Наконец, на одном и том же источнике для одного и того же события может быть несколько обработчиков. Конечно, это слегка глупо так делать - разбивать свой обработчик на несколько маленьких. Но так и будет, если чужой код захочет повесить обработчик на ваш префаб. Вы сами так делаете, когда вешаете свои обработчики на префабы игры. Поэтому адрес функции тоже является ключом для удаления. Категория:Модификации